# Race Condition
# Race Condition (состояние гонки) - это состояние, когда два или более потока (или корутины) пытаются получить
# доступ к одному и тому же ресурсу одновременно. Это может привести к непредсказуемому поведению программы,
# такому как повреждение данных, блокировки потока или даже сбоям программы. В Python, где многопоточность и 
# многозадачность являются ключевыми особенностями, Race Condition может возникнуть в любом месте, где доступ к
# общему ресурсу не синхронизирован, при этом вы не получите сообщения об ошибке, ваши данные просто будут
# изменяться не в той последовательности, в которой необходимо. Это означает, что поведение программы будет 
# непредсказуемым и может меняться при каждом запуске.


# Аналогия состояния гонки (race condition) в реальной жизни может быть следующей:
# Представьте, что у вас есть ресурс, например, банковский счет, к которому имеют доступ несколько человек одновременно.
# Каждый человек хочет снять с этого счета деньги и увеличить свой собственный баланс (сделать перевод с общего на личный счёт).

# Они выполняют следующие шаги:
# Человек A проверяет баланс счета и видит, что на счету 1000 рублей.
# Человек B также проверяет баланс и видит тот же самый результат — 1000 рублей.
# Оба человека решают снять 900 рублей со счета.
# Человек A делает запрос на снятие 900 рублей и видит, что баланс счета равен 100 рублей.
# Человек B также делает запрос на снятие 900 рублей и также видит, что баланс счета равен 100 рублей.
# Оба человека выполняют операцию снятия 900 рублей, в итоге уменьшая баланс счета до -800 рублей.

# Человек A и B проверяют баланс счета и видят одно и то же значение. Однако они не синхронизируют свои действия, и, как результат,
# происходит двойное снятие денег, что приводит к некорректному состоянию счета с отрицательным балансом.

# Следующий пример выдуманный, он является показательным для более широкого класса гонок (race conditions) между корутинами при использовании общего ресурса. 

# Пример кода с race condition и банковским счётом: 
import asyncio    

# Инициализация банковского счета
bank_account = 1000                   

# Асинхронная функция для снятия денег
async def withdraw_money(amount):     
    global bank_account    
    # Проверка наличия достаточных средств на счете           
    if bank_account >= amount:        
        print(f"Снятие {amount} р. успешно") 
        await asyncio.sleep(1)     

        # Вычитание суммы снятия из общего банковского счета   
        bank_account -= amount        

async def main():                     
    task1 = asyncio.create_task(withdraw_money(900))                                     
    task2 = asyncio.create_task(withdraw_money(900))  
    await asyncio.gather(task1, task2)  
                                        
    print(f'Остаток средств {bank_account} р.')  

asyncio.run(main()) 

# Вывод:

# Снятие 900 р. успешно
# Снятие 900 р. успешно
# Остаток средств -800 р.
# Ключевой момент, на который стоит обратить внимание — это возникновение состояния гонки (race condition),
# когда две корутины пытаются одновременно снять деньги с одного и того же счета. Из-за асинхронной природы 
# операций и задержки в выполнении (имитируемой с помощью asyncio.sleep(1)), обе задачи проверяют условие
# наличия средств на счете до того, как любая из них успевает фактически изменить баланс счета. Это приводит
# к тому, что обе операции снятия считаются успешными, несмотря на то, что после выполнения обеих операций
# суммарный остаток на счете не соответствует ожидаемому. В итоге, это может привести к отрицательному балансу,
# что в реальных условиях является критической ошибкой в системах, работающих с финансовыми операциями.

#Пример кода, решающий проблему race condition с использованием asyncio.Lock()

import asyncio

bank_account = 1000


# Принимает сумму для снятия и блокировку для безопасного доступа к банковскому счету
async def withdraw_money(amount, lock):
    global bank_account

    # Используем асинхронную блокировку для безопасного доступа к банковскому счету
    async with lock:
        if bank_account >= amount:
            print(f"Снятие {amount} р. успешно")
            await asyncio.sleep(1)
            bank_account -= amount


async def main():
    lock = asyncio.Lock()
    task1 = asyncio.create_task(withdraw_money(900, lock))
    task2 = asyncio.create_task(withdraw_money(900, lock))


    await asyncio.gather(task1, task2)
    print(f'Остаток средств {bank_account} р.')


asyncio.run(main())

# Вывод:

# Снятие 900 р. успешно
# Остаток средств 100 р.
# В код внесены следующие изменения:

# Теперь в функции withdraw_money(), перед совершением операции снятия, происходит проверка наличия необходимых средств.

# # Используем асинхронную блокировку для безопасного доступа к банковскому счету
# async with lock:
#     if bank_account >= amount:
#     ...
#     ...
# Важной частью является использование асинхронной блокировки lock, благодаря которой одновременно может быть
# выполнена только одна операция снятия средств. Это предотвращает возможность одновременного снятия средств,
# которое могло бы привести к некорректному расчёту остатка на счету, если бы операции выполнялись параллельно без синхронизации. 